来源:香依香偎@闻道解惑
Mozilla Rhino 是一个完全使用 Java 语言编写的开源 JavaScript 引擎。ysoserial 中收录了 Rhino 的反序列化 Gadget,本篇文章就来分析一下这个 Gadget。
零、NativeError 的继承关系
首先来看 org.mozilla.javascript.NativeError
类的继承关系。它继承自 IdScriptableObject
,后者继承自 ScriptableObject
。而 ScriptableObject
实现了 Scriptable
接口和 Serializable
接口。因此,NativeError
可以进行序列化和反序列化操作。
一、分析
1、 首先,反序列化攻击的入口在 NativeError
的 toString()
函数。
toString()
中调用了 js_toString()
函数,传入参数为 NativeError
的 this 对象。看下js_toString()
。
js_toString()
调用了两次 getString()
函数,传入的参数是 NativeError
对象和字符串 name/message,继续跟进。
getString()
中调用的是父类 ScriptableObject
的 getProperty()
函数,入参没有变化。跟进去看看。
其中调用的是 Scriptable
接口的 get()
函数。这个 get()
的实现在 IdScriptableObject
类。
IdScriptableObject.get()
最后调用的是父类 ScriptableObject
的 get()
函数,再次回到 ScriptableObject
类。
继续跟进 getImpl()
函数。
其中的关键在于 2007 行到 2026 行的这部分。先看 2009 行到 2020 行的第一个分支。
这个分支中有 nativeGetter.invoke()
的调用,看上去有戏。但有一个问题在于,nativeGetter.delegateTo
是 transient
变量,在反序列化过程中无法赋值。
这会导致 2013 行 if (nativeError.delegateTo == null)
的判断恒真,getterThis
就被赋值为最初的 NativeError
对象。这就导致 2020 行的 nativeGetter.invoke()
无法调用我们期望的目标对象的函数,只能调用静态函数或者 NativeError
类的内置函数。这当然不是我们期望的结果。
再来看 2021 行到 2026 行的 else
分支。
这个分支中需要将 getterObj
设置为 Function
对象,并最终调用 Function
的 call()
函数。先看看 getterObj
如何赋值。
通过 GetterSlot
的 getter
。GetterSlot
是 ScriptableObject
的内部类,支持序列化。
GetterSlot.getter
可以通过 ScriptableObject.setGetterOrSetter()
来进行赋值。
那么 getterObj
要赋值成 Function
的哪个对象呢?Function
是个接口,看下它的实现类。
我们选择 NativeJavaMethod
类。这个类继承自 BaseFunction
,后者同样继承自 IdScriptableObject
,因此同样可以进行序列化和反序列化处理。
NativeJavaMethod.call()
函数挺长,翻一翻会发现在 247 行调用了 meth.invoke(javaObject, args)
。
这个 invoke()
的调用,其实是 MemberBox.invoke()
函数,其中直接调用了我们熟悉的 method.invoke()
函数。
看起来很有希望。为了能成功调用到我们期望的目标函数,我们需要关注 NativeJavaMethod.call()
中 meth.invoke(javaObject, args)
里的三个变量:meth
、javaObject
和args
。
一个一个来,先看 meth
。
2、meth
的值来自类的成员变量 methods
,通过 findFunction()
查找到索引 index
。
成员变量 methods
是 MemberBox
类的对象数组,本身可以通过反序列化赋值。
至于 methods
的内容要设置成什么样,来看下 MemberBox.invoke()
函数。其中 method
来自 method()
函数,而后者是直接返回了 memberObject
变量。
MemberBox.memberObject
是个 transient 变量,要怎么赋值呢?
答案就在 MemberBox.readObject()
中。这里先通过 readMember()
得到了 member 对象,再通过 init()
函数将 member
赋值给 memberObject
。
继续跟进 readMember()
函数,就是一个反序列化的实现。因此,通过反序列化给 memberObject
的赋值,不存在问题。
也就是说,我们可以通过反序列化给 meth
赋值为期望的目标函数。
结论
设置 NativeJavaMethod.call()
中的 meth
需要:
- 构造
MemberBox
对象 m - 设置 m 的成员变量
memberObject
为目标函数 - 构造
NativeJavaMethod
对象 n - 设置 n 的成员变量
methods
的 0 号元素为 m
3、 javaObject 涉及的代码,都在 NativeJavaMethod.call()
的 222~247 行。
关键的部分就是 225~242 行的 else
分支里。
如果要把 javaObject
赋值为我们期望的对象,就是要在 235 行完成这个赋值。但是这里有一个问题:我们知道 thisObj
就是 NativeError
对象,同理 o
也是。但 NativeError
没有实现 Wrapper
接口,这样一来 234 行的判断条件 if (o instanceof Wrapper)
就不能满足了。
转机在于,这个判断身处循环之中,240 行的 o = o.getPrototype()
给了我们希望。查看一下 Wrapper
的实现类。
看下 NativeJavaObject
的 unwrap()
函数,直接返回了 NativeJavaObject.javaObject
成员变量。
而 NativeJavaObject.javaObject
成员变量可以通过反序列化的 readObject()
函数直接赋值。
也就是说,如果我们让 NativeError
对象的 getPrototype()
返回特定的 NativeJavaObject
对象,就可以完成 javaObject
的赋值。看看 getPrototype()
的实现,在 ScriptableObject
类中。
这个 prototypeObject
来自 ScriptableObject 的成员变量,可以通过反序列化赋值。
结论
设置 NativeJavaMethod.call()
中的 javaObject
需要:
- 构造
NativeJavaObject
对象 o - 设置 o 的成员变量
javaObject
为目标对象 - 构造
NativeError
对象 e - 设置 e 的成员变量
prototypeObject
为 o
4、 最后看一下 args
。args
来自入参,其实就是调用者传入的 ScriptRuntime.emptyArgs
。
这就决定我们要寻找的目标函数,必须是一个无参函数。
5、 再回到开头,通常反序列化的入口都是 readObject()
函数,而文章开头说 NativeError
的反序列化入口在 toString()
函数。怎么才能从 readObject()
入口转到 NativeError.toString()
呢?
答案就在 JDK 中的 BadAttributeValueExpException
类的 readObject()
函数。
也就是说,只要将 BadAttributeValueExpException
的 val
设置为 NativeError
对象,就可以在反序列化的过程中调用 NativeError.toString()
了。
6、结论
如果要完成反序列化POC,需要:
- 构造
MemberBox
对象 m - 设置 m 的成员变量
memberObject
为目标函数 - 构造
NativeJavaMethod
对象 n - 设置 n 的成员变量
methods
的 0 号元素为 m - 构造
NativeJavaObject
对象 o - 设置 o 的成员变量
javaObject
为目标对象 - 构造
NativeError
对象 a - 设置 a 的成员变量
prototypeObject
为 o - 通过 a 的
setGetterOrSetter()
函数,设置 a 的getter
属性为对象 n - 构造
BadAttributeValueExpException
对象 b - 设置 b 的成员变量
val
为NativeError
对象 a
前面说过,需要寻找的目标函数,应当是一个无参函数。同时,这个无参函数所属的目标类,还得是实现了 Serializable
接口、支持序列化和反序列化的类。
因此,首先想到的就是,使用 TemplatesImpl
类作为目标类,使用它的 getOutputProperties()
作为目标函数。
二、填坑
完成了上述分析,我们开始写POC。途中暗坑无数,逐一填之。
1、NativeError
无法实例化
声明 NativeError
对象,直接报错:The type NativeError is not visible
。
报错原因:
NativeError
类不是 public
,不能直接引用。
解决方案:
通过反射,实例化 NativeError
对象。
2、反射实例化的 NativeError
运行失败
运行这段代码:
报错 “ Class com.xiang.rhinotest.RhinoPoc can not access a member of class org.mozilla.javascript.NativeError with modifiers “” ”
报错原因:
NativeError
没有提供默认的public 无参构造函数,无法直接调用 newInstance()。
解决方案:
通过反射设置构造函数为 public,再进行调用。
反射在 ysoserial 中被大量的使用,原因也就在此。
3、执行POC失败:No Context
按照“分析”部分的结论,结合大量的反射调用,完成POC如下。
1 | private static Object generate_Object() throws Exception { |
编译运行。呃,序列化成功,可是反序列化的时候却没看到计算器,只看到了报错:“No Context associated with current Thread”。
报错原因:
问题在哪里呢?就在 ScriptableObject.getImpl()
的 else 分支中。
我们期望进入 2024 行的 f.call()
,结果在 2023 行 Context.getContext()
抛出了异常,因为 Context 对象为空。
构造 Context
需要调用 Context.enter()
函数。
怎样在反序列化的时候插入 Context.enter()
的调用呢?
重新看下调用栈,发现 NativeError.js_toString()
调用了两次 getString()
函数,分别传入字符串 “name”和“message”。
因此,我们可以把 TemplatesImpl.getOutputProperties()
作为 “message”的属性,把 Context.enter()
作为 “name” 的属性,这样就可以先执行 Context.enter()
,再执行 TemplatesImpl.getOutputProperties()
进行 Payload 执行。
解决方案:
按照 POC 中设置 TemplatesImpl.getOutputProperties()
的方法,设置 Context.enter()
为 “name” 属性,将 TemplatesImpl.getOutputProperties()
设置为 “message” 属性
1 | //构造 MemberBox 对象 m |
4、执行POC仍然失败:No Context
增加 Context.enter()
的调用之后,重新运行POC,呃,问题依旧……
报错原因:
为什么新增的调用无效呢?因为设置函数的方法错了。
无论是 TemplatesImpl.getOutputProperties()
还是 Context.enter()
,我们都是通过 ScriptableObject.setGetterOrSetter()
函数进行设置。而这个函数设置的 getter
属性,是 Callable
类型的。
回到报错的地方看。 2009 行 if 分支的判断条件是,getter
属性的值必须是 MemberBox
类型,而 MemberBox
并没有实现 Callable
接口,所以无论进来的是TemplatesImpl.getOutputProperties()
还是 Context.enter()
,代码流程都会走到 2021 行的 else 分支中。
我们期望流程走到 2024 行的 f.call()
,遇到的问题是在 2023 行就报错了。我们增加 Context.enter()
的调用,期望他能解决无法通过 f.call()
来调用TemplatesImpl.getOutputProperties()
的问题。
但是对 Context.enter()
的调用遇到了一样的问题,在 2023 行就抛出了异常,无法走到 2024 行去执行我们期望的函数。
所以,对 Context.enter()
的设置,就不能像 TemplatesImpl.getOutputProperties()
一样,去通过 ScriptableObject.setGetterOrSetter()
函数进行设置,只能让他通过 2009 行的 if 分支去调用。但是要怎么去设置呢?ysoserial 通过反射进行强制设置 getter
属性来解决这个问题。
解决方案:
参考 ysoserial 中的方法,通过反射进行强制设置 getter
属性为 MemberBox 对象的 Context.enter()
方法:
1 | //构造 MemberBox 对象 m2 |
现在再执行 POC,终于可以看到计算器了。
三、POC
完整POC参见 Github。
主要函数:
1 |
|
调用栈:
四、心得
1、 有 BadAttributeValueExpException
作为反序列化的入口,toString()
也成为了 readObject()
之外的另一个反序列化攻击触发点。
2、 反射功能,很好很强大。